 /*******************************************************************************
  * Copyright (c) 2004, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.localstore;

 import java.io.*;
 import java.util.*;
 import org.eclipse.core.internal.resources.ResourceException;
 import org.eclipse.core.internal.resources.ResourceStatus;
 import org.eclipse.core.internal.utils.Messages;
 import org.eclipse.core.resources.IResourceStatus;
 import org.eclipse.core.runtime.*;
 import org.eclipse.osgi.util.NLS;

 /**
  * A bucket is a persistent dictionary having paths as keys. Values are determined
  * by subclasses.
  *
  * @since 3.1
  */
 public abstract class Bucket {

     public static abstract class Entry {
         /**
          * This entry has not been modified in any way so far.
          *
          * @see #state
          */
         private final static int STATE_CLEAR = 0;
         /**
          * This entry has been requested for deletion.
          *
          * @see #state
          */
         private final static int STATE_DELETED = 0x02;
         /**
          * This entry has been modified.
          *
          * @see #state
          */
         private final static int STATE_DIRTY = 0x01;

         /**
          * Logical path of the object we are storing history for. This does not
          * correspond to a file system path.
          */
         private IPath path;

         /**
          * State for this entry. Possible values are STATE_CLEAR, STATE_DIRTY and STATE_DELETED.
          *
          * @see #STATE_CLEAR
          * @see #STATE_DELETED
          * @see #STATE_DIRTY
          */
         private byte state = STATE_CLEAR;

         protected Entry(IPath path) {
             this.path = path;
         }

         public void delete() {
             state = STATE_DELETED;
         }

         public abstract int getOccurrences();

         public IPath getPath() {
             return path;
         }

         public abstract Object getValue();

         public boolean isDeleted() {
             return state == STATE_DELETED;
         }

         public boolean isDirty() {
             return state == STATE_DIRTY;
         }

         public boolean isEmpty() {
             return getOccurrences() == 0;
         }

         public void markDirty() {
             Assert.isTrue(state != STATE_DELETED);
             state = STATE_DIRTY;
         }

         /**
          * Called on the entry right after the visitor has visited it.
          */
         public void visited() {
             // does not do anything by default
 }
     }

     /**
      * A visitor for bucket entries.
      */
     public static abstract class Visitor {
         // should continue the traversal
 public final static int CONTINUE = 0;
         // should stop looking at states for files in this container (or any of its children)
 public final static int RETURN = 2;

         /**
          * Called after the bucket has been visited (and saved).
          */
         public void afterSaving(Bucket bucket) throws CoreException {
             // empty implementation, subclasses to override
 }

         public void beforeSaving(Bucket bucket) throws CoreException {
             // empty implementation, subclasses to override
 }

         /**
          * @return either STOP, CONTINUE or RETURN
          */
         public abstract int visit(Entry entry);
     }

     /**
      * The segment name for the root directory for index files.
      */
     static final String INDEXES_DIR_NAME = ".indexes"; //$NON-NLS-1$

     /**
      * Map of the history entries in this bucket. Maps (String -> byte[][]),
      * where the key is the path of the object we are storing history for, and
      * the value is the history entry data (UUID,timestamp) pairs.
      */
     private final Map entries;
     /**
      * The file system location of this bucket index file.
      */
     private File location;
     /**
      * Whether the in-memory bucket is dirty and needs saving
      */
     private boolean needSaving = false;
     /**
      * The project name for the bucket currently loaded. <code>null</code> if this is the root bucket.
      */
     protected String projectName;

     public Bucket() {
         this.entries = new HashMap();
     }

     /**
      * Applies the given visitor to this bucket index.
      * @param visitor
      * @param filter
      * @param depth the number of trailing segments that can differ from the filter
      * @return one of STOP, RETURN or CONTINUE constants
      * @exception CoreException
      */
     public final int accept(Visitor visitor, IPath filter, int depth) throws CoreException {
         if (entries.isEmpty())
             return Visitor.CONTINUE;
         try {
             for (Iterator i = entries.entrySet().iterator(); i.hasNext();) {
                 Map.Entry mapEntry = (Map.Entry) i.next();
                 IPath path = new Path((String ) mapEntry.getKey());
                 // check whether the filter applies
 int matchingSegments = filter.matchingFirstSegments(path);
                 if (!filter.isPrefixOf(path) || path.segmentCount() - matchingSegments > depth)
                     continue;
                 // apply visitor
 Entry bucketEntry = createEntry(path, mapEntry.getValue());
                 // calls the visitor passing all uuids for the entry
 int outcome = visitor.visit(bucketEntry);
                 // notify the entry it has been visited
 bucketEntry.visited();
                 if (bucketEntry.isDeleted()) {
                     needSaving = true;
                     i.remove();
                 } else if (bucketEntry.isDirty()) {
                     needSaving = true;
                     mapEntry.setValue(bucketEntry.getValue());
                 }
                 if (outcome != Visitor.CONTINUE)
                     return outcome;
             }
             return Visitor.CONTINUE;
         } finally {
             visitor.beforeSaving(this);
             save();
             visitor.afterSaving(this);
         }
     }

     /**
      * Tries to delete as many empty levels as possible.
      */
     private void cleanUp(File toDelete) {
         if (!toDelete.delete())
             // if deletion didn't go well, don't bother trying to delete the parent dir
 return;
         // don't try to delete beyond the root for bucket indexes
 if (toDelete.getName().equals(INDEXES_DIR_NAME))
             return;
         // recurse to parent directory
 cleanUp(toDelete.getParentFile());
     }

     /**
      * Factory method for creating entries. Subclasses to override.
      */
     protected abstract Entry createEntry(IPath path, Object value);

     /**
      * Flushes this bucket so it has no contents and is not associated to any
      * location. Any uncommitted changes are lost.
      */
     public void flush() {
         projectName = null;
         location = null;
         entries.clear();
         needSaving = false;
     }

     /**
      * Returns how many entries there are in this bucket.
      */
     public final int getEntryCount() {
         return entries.size();
     }

     /**
      * Returns the value for entry corresponding to the given path (null if none found).
      */
     public final Object getEntryValue(String path) {
         return entries.get(path);
     }

     /**
      * Returns the file name used to persist the index for this bucket.
      */
     protected abstract String getIndexFileName();

     /**
      * Returns the version number for the file format used to persist this bucket.
      */
     protected abstract byte getVersion();

     /**
      * Returns the file name to be used to store bucket version information
      */
     protected abstract String getVersionFileName();

     /**
      * Loads the contents from a file under the given directory.
      */
     public void load(String newProjectName, File baseLocation) throws CoreException {
         load(newProjectName, baseLocation, false);
     }

     /**
      * Loads the contents from a file under the given directory. If <code>force</code> is
      * <code>false</code>, if this bucket already contains the contents from the current location,
      * avoids reloading.
      */
     public void load(String newProjectName, File baseLocation, boolean force) throws CoreException {
         try {
             // avoid reloading
 if (!force && this.location != null && baseLocation.equals(this.location.getParentFile()) && (projectName == null ? (newProjectName == null) : projectName.equals(newProjectName))) {
                 this.projectName = newProjectName;
                 return;
             }
             // previously loaded bucket may not have been saved... save before loading new one
 save();
             this.projectName = newProjectName;
             this.location = new File(baseLocation, getIndexFileName());
             this.entries.clear();
             if (!this.location.isFile())
                 return;
             DataInputStream source = new DataInputStream(new BufferedInputStream(new FileInputStream(location), 8192));
             try {
                 int version = source.readByte();
                 if (version != getVersion()) {
                     // unknown version
 String message = NLS.bind(Messages.resources_readMetaWrongVersion, location.getAbsolutePath(), Integer.toString(version));
                     ResourceStatus status = new ResourceStatus(IResourceStatus.FAILED_READ_METADATA, message);
                     throw new ResourceException(status);
                 }
                 int entryCount = source.readInt();
                 for (int i = 0; i < entryCount; i++)
                     this.entries.put(readEntryKey(source), readEntryValue(source));
             } finally {
                 source.close();
             }
         } catch (IOException ioe) {
             String message = NLS.bind(Messages.resources_readMeta, location.getAbsolutePath());
             ResourceStatus status = new ResourceStatus(IResourceStatus.FAILED_READ_METADATA, null, message, ioe);
             throw new ResourceException(status);
         }
     }

     private String readEntryKey(DataInputStream source) throws IOException {
         if (projectName == null)
             return source.readUTF();
         return IPath.SEPARATOR + projectName + source.readUTF();
     }

     /**
      * Defines how data for a given entry is to be read from a bucket file. To be implemented by subclasses.
      */
     protected abstract Object readEntryValue(DataInputStream source) throws IOException, CoreException;

     /**
      * Saves this bucket's contents back to its location.
      */
     public void save() throws CoreException {
         if (!needSaving)
             return;
         try {
             if (entries.isEmpty()) {
                 needSaving = false;
                 cleanUp(location);
                 return;
             }
             // ensure the parent location exists
 File parent = location.getParentFile();
             if (parent == null)
                 throw new IOException();//caught and rethrown below
 parent.mkdirs();
             DataOutputStream destination = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(location), 8192));
             try {
                 destination.write(getVersion());
                 destination.writeInt(entries.size());
                 for (Iterator i = entries.entrySet().iterator(); i.hasNext();) {
                     Map.Entry entry = (Map.Entry) i.next();
                     writeEntryKey(destination, (String ) entry.getKey());
                     writeEntryValue(destination, entry.getValue());
                 }
             } finally {
                 destination.close();
             }
             needSaving = false;
         } catch (IOException ioe) {
             String message = NLS.bind(Messages.resources_writeMeta, location.getAbsolutePath());
             ResourceStatus status = new ResourceStatus(IResourceStatus.FAILED_WRITE_METADATA, null, message, ioe);
             throw new ResourceException(status);
         }
     }

     /**
      * Sets the value for the entry with the given path. If <code>value</code> is <code>null</code>,
      * removes the entry.
      */
     public final void setEntryValue(String path, Object value) {
         if (value == null)
             entries.remove(path);
         else
             entries.put(path, value);
         needSaving = true;
     }

     private void writeEntryKey(DataOutputStream destination, String path) throws IOException {
         if (projectName == null) {
             destination.writeUTF(path);
             return;
         }
         // omit the project name
 int pathLength = path.length();
         int projectLength = projectName.length();
         String key = (pathLength == projectLength + 1) ? "" : path.substring(projectLength + 1); //$NON-NLS-1$
 destination.writeUTF(key);
     }

     /**
      * Defines how an entry is to be persisted to the bucket file.
      */
     protected abstract void writeEntryValue(DataOutputStream destination, Object entryValue) throws IOException, CoreException;
 }

